/*
 * Copyright 2010 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.websocket;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.Collections;
import java.util.Map;

/**
 * Creates WebSocket connections by listening on a port and selecting the
 * correct handshake to use for a given client connection.
 */
public class HttpWebSocketListener {

  /**
   * Callbacks for new connections.
   */
  public interface ListenCallback {

    /**
     * Called when a new WebSocket connection is successfully initialized, on
     * the thread on which
     * {@link HttpWebSocketListener#listen(int, ListenCallback, HttpCompatibleHandshake...)}
     * was called.
     * 
     * @param socket
     * @param remoteSocketAddress
     */
    void onConnection(WebSocket socket, SocketAddress remoteSocketAddress);

    /**
     * Called when an exception occurs accepting the socket or during the
     * WebSocket handshake process.
     * 
     * @param remoteAddress
     * @param e
     */
    void onException(SocketAddress remoteAddress, IOException e);
  }

  /**
   * Listen for incoming socket connections.  Does not return until
   * {@link ServerSocket#accept()} fails (such as when closed), and will make
   * all callbacks on the thread it is invoked with.  It tries one or more
   * 
   * @param bindPort
   * @param callback
   * @param handshakes list of handshakes which to try
   * @throws IOException only for setting up the socket - later exceptions are
   *     delivered to the callback
   */
  // TODO(jat): support extensions, acceptable origins, etc
  public static void listen(int bindPort, ListenCallback callback,
      HttpCompatibleHandshake... handshakes) throws IOException {
    listen(Util.getBoundServerSocket(bindPort), callback, handshakes);
  }

  /**
   * Listen for incoming socket connections.  Does not return until
   * {@link ServerSocket#accept()} fails (such as when closed), and will make
   * all callbacks on the thread it is invoked with.  It tries one or more
   * 
   * @param listenSocket
   * @param callback
   * @param handshakes list of handshakes which to try
   * @throws IOException only for setting up the socket - later exceptions are
   *     delivered to the callback
   */
  // TODO(jat): support extensions, acceptable origins, etc
  public static void listen(ServerSocket listenSocket, ListenCallback callback,
      HttpCompatibleHandshake... handshakes) throws IOException {
    while (true) {
      Socket clientSocket = null;
      SocketAddress remoteAddress = null;
      try {
        clientSocket = listenSocket.accept();
        clientSocket.setKeepAlive(true);
        clientSocket.setTcpNoDelay(true);
        remoteAddress = clientSocket.getRemoteSocketAddress();
        InputStream fromClient = clientSocket.getInputStream();
        OutputStream toClient = clientSocket.getOutputStream();
        String requestLine = Util.readUtf8Line(fromClient);
        Map<String, String> headers = Util.readHeaderFields(fromClient);
        boolean found = false;
        for (HttpCompatibleHandshake handshake : handshakes) {
          if (handshake.matches(requestLine, headers)) {
            WebSocket socket = handshake.serverHandshake(requestLine, headers,
                fromClient, toClient, Collections.<Extension>emptySet(),
                listenSocket.getLocalPort());
            callback.onConnection(socket, remoteAddress);
            found = true;
            break;
          }
        }
        if (!found) {
          throw new IOException("Connection with no supported handshake"); 
        }
      } catch (IOException e) {
        callback.onException(remoteAddress, e);
      }
    }
  }
}
